﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web.Mvc;
using System.Web.Mvc.Html;
using System.Web.Routing;
using System.Reflection;
using System.Web;
using System.ComponentModel;
using System.IO;
using System.Web.Helpers;

namespace MVC.ApiExplorer
{
	public class ApiExploreView : IView
	{
		private IList<MethodInfo> model;
		public ApiExploreView(IList<MethodInfo> model):base()
		{
			this.model = model;
		}

		public void Render(ViewContext viewContext, TextWriter writer)
		{
			HtmlHelper htmlHelper = new HtmlHelper(viewContext, new ViewPage());

			var routeName = viewContext.ViewData["routeName"].ToString();
			var controllerName = viewContext.ViewData["controllerName"].ToString();
			StringBuilder html= new StringBuilder();
			html.AppendFormat(
@"
<!DOCTYPE html PUBLIC ""-//W3C//DTD XHTML 1.0 Transitional//EN"" ""http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"">
<html xmlns=""http://www.w3.org/1999/xhtml"">
<head>
	<title>MVC.ApiExplorer</title>
	<style type=""text/css"">
		body,div,dl,dt,dd,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,form,fieldset,input,textarea,p,blockquote,th,td {{ margin:0; padding:0; }}
		table {{border-collapse:collapse; border-spacing:0;}}
		fieldset,img {{border:0;}}
		address,caption,cite,code,dfn,em,strong,th,var {{font-style:normal; font-weight:normal;}}
		ol,ul {{list-style:none;}}
		caption,th {{text-align:left;}}
		h1,h2,h3,h4,h5,h6 {{font-size:100%; font-weight:bold;}}
		q:before,q:after {{content:'';}}
		abbr,acronym {{ border:0;}}

		body {{font-size:11pt; background:#fff; height:100%;}}
		h1{{font-size:18pt;}}
		h2{{font-size:16pt;}}
		
		td{{padding:2px;}}
		input[type=submit]{{padding:3px;}}
		input[type=radio]{{margin:0 0 0 4px;}}
		strong{{font-weight:bold;}}
		iframe{{width:100%; height:95%; border:solid 1px #000; background:#fff;}}

		#header{{
			background-color:#2d2d2d;
			color:#fff;
			border:solid 1px #000;
			padding: 8px;
		}}

		#caption{{
			background-color:#f1f1f1;
			border-bottom: solid 1px #d2d2d2;
			padding: 5px;
		}}

		#api-area{{padding:10px;width:600px;border:solid 0px;float:left;}}

		#api-area .api
		{{
			border-bottom:solid 1px #ddd;
			padding:10px 5px;
		}}
		#api-area .api:last-child
		{{
			border-bottom:0px;
		}}
		
		.api-name
		{{
			color:#36c;
		}}
		
		.api-fields
		{{
			background-color:#f3f3f3;
			border-left:solid 1px #36c;
			padding:5px;
		}}
		.api-subfields
		{{
			border:solid 1px #d2d2d2;
			padding:5px;
		}}

		#console{{
			padding:10px;
			width:550px;
			border:solid 0px;
			position:fixed;
			top 0px;
			left:620px;
			height:85%;
		}}
		#console iframe
		{{
			border:solid 1px #2d2d2d;
		}}
	</style>
</head>
<body>
<h1 id=""header"">MVC.ApiExplorer - ver {0}</h1>
	<h2 id=""caption"">{1} controller</h2>
<div id=""api-area"">
	<ul>
", this.GetType().Assembly.GetName().Version.ToString(), controllerName);

			
			foreach (MethodInfo m in (model))
			{
				var actionName = getActionName(m);
				var actionUrl = getActionUrl(routeName, viewContext, controllerName, actionName);

				html.AppendFormat(@"<li class=""api"">({1}) <strong class=""api-name"">{0}</strong><br />", actionName, m.ReturnType.Name);
				html.AppendFormat("<p>{0}</p>", getDescription(m));
				renderHttpMethod(html, controllerName, actionName);
				html.AppendFormat(@"<form id=""{0}_{1}_form"" action=""{2}"" method=""get"" target=""output""  enctype=""multipart/form-data"">", controllerName, actionName, actionUrl);
				renderAntiForgeryToken(viewContext, html, m);
				html.Append("<table cellspacing=\"0\">");
				var parameters = m.GetParameters();
				foreach (var parameter in parameters)
				{
					Type fieldType = parameter.ParameterType;
					string fieldName = parameter.Name;
					string fieldDescription = getDescription(parameter);

					renderField(htmlHelper, html, 0, fieldType, "", fieldName, fieldDescription);
				}

				html.Append("</table>");
				html.AppendFormat(@"<input type=""submit"" value=""Invok"" /> {0} </form></fieldset></li>", actionUrl);
			}


			html.Append(
@"
	</ul>
</div>
<div id=""console"">
<h2>Action result</h2>
<iframe src="""" name=""output""></iframe>
</div>
</body>
</html>");


			writer.Write(html.ToString());
		}
		private static void renderComplexField(HtmlHelper htmlHelper, StringBuilder html, string FieldNamespace, PropertyInfo[] properties, int deep)
		{
			html.AppendFormat(@"<fieldset class=""api-subfields""><legend>{0}:</legend>", FieldNamespace);
			html.Append("<table cellspacing=\"0\">");
			foreach (var property in properties)
			{
				Type fieldType = property.PropertyType;
				string fieldName = property.Name;
				string fieldDescription = getDescription(property);

				renderField(htmlHelper, html, deep, fieldType, FieldNamespace, fieldName, fieldDescription);
			}
			html.Append("</table>");
			html.Append(@"</fieldset>");
		}
		private static void renderField(HtmlHelper htmlHelper, StringBuilder html, int deep, Type fieldType, string fieldNamespace, string fieldName, string fieldDescription)
		{
			html.Append("<tr>");
			if (isGeneralField(fieldType))
			{
				renderInput(htmlHelper, html, fieldType, fieldNamespace, fieldName, fieldDescription, false);
			}
			else if (isFileUploadField(fieldType))
			{
				renderInput(htmlHelper, html, fieldType, fieldNamespace, fieldName, fieldDescription, true);
			}
			else
			{
				//Complex model binding
				if (isDerivedFromGenericType(fieldType, typeof(IEnumerable<>)))
				{
					//Enumerable type
					var innerFieldType = fieldType.GetGenericArguments()[0];
					bool isComplexType = !isGeneralField(innerFieldType) && !isFileUploadField(innerFieldType);
					
					for (int i = 0; i < 3; i++)
					{
						html.Append(htmlHelper.Hidden(fieldName + ".Index", i).ToHtmlString());
						string enumerablefieldName = string.Format("{0}[{1}]", fieldName, i);
						renderField(htmlHelper, html, deep, innerFieldType, fieldNamespace, enumerablefieldName, fieldDescription);
					}
				}
				else
				{
					//sub class
					html.Append("<td colspan=\"3\">");
					if (deep < 3)
					{
						deep++;
						var subProperties = fieldType.GetProperties(BindingFlags.Instance | BindingFlags.Public);
						fieldNamespace = string.IsNullOrEmpty(fieldNamespace) ? fieldName : string.Format("{0}.{1}", fieldNamespace, fieldName);
						renderComplexField(htmlHelper, html, fieldNamespace, subProperties, deep);
					}
					html.Append("</td>");
				}
			}

			html.Append("</tr>");
		}

		private static void renderInput(HtmlHelper htmlHelper, StringBuilder html, Type fieldType, string fieldNamespace, string fieldName, string fieldDescription, bool isFileUpload)
		{
			var name = string.IsNullOrEmpty(fieldNamespace) ? fieldName : string.Format("{0}.{1}", fieldNamespace, fieldName);
			html.AppendFormat(@"<td>({1}) <span>{0}:</span></td><td>{3}</td><td>{2}</td>",
				name,
				simplifyTypeName(fieldType),
				fieldDescription,
				isFileUpload ? htmlHelper.TextBox(name, "", new { type="file"}) : htmlHelper.TextBox(name)
				);
		}
		private static void renderHttpMethod(StringBuilder html, string controllerName, string methodName)
		{
			html.AppendFormat(
@"<fieldset class=""api-fields"">
<input type=""radio"" id=""{0}_{1}_get"" name=""{0}_{1}"" checked=""checked"" onchange=""document.getElementById('{0}_{1}_form').setAttribute('method','get');"" ><label for=""{0}_{1}_get"">GET</label> 
<input type=""radio"" id=""{0}_{1}_post"" name=""{0}_{1}"" onchange=""var form = document.getElementById('{0}_{1}_form'); form.setAttribute('method','post'); var inputTag = document.getElementById('{0}_{1}_put_http_method_override'); if(inputTag){{form.removeChild(inputTag);}} inputTag = document.getElementById('{0}_{1}_delete_http_method_override'); if(inputTag){{form.removeChild(inputTag);}}"" ><label for=""{0}_{1}_post"">POST</label>
<input type=""radio"" id=""{0}_{1}_put"" name=""{0}_{1}"" onchange=""var form = document.getElementById('{0}_{1}_form'); form.setAttribute('method','post'); var inputTag = document.getElementById('{0}_{1}_delete_http_method_override'); if(inputTag){{form.removeChild(inputTag);}} inputTag = document.createElement('input'); inputTag.id='{0}_{1}_put_http_method_override'; inputTag.type='hidden'; inputTag.name='X-HTTP-Method-Override'; inputTag.value='PUT'; form.appendChild( inputTag );"" ><label for=""{0}_{1}_put"">PUT</label> 
<input type=""radio"" id=""{0}_{1}_delete"" name=""{0}_{1}"" onchange=""var form = document.getElementById('{0}_{1}_form'); form.setAttribute('method','post'); var inputTag = document.getElementById('{0}_{1}_put_http_method_override'); if(inputTag){{form.removeChild(inputTag);}} inputTag = document.createElement('input'); inputTag.id='{0}_{1}_delete_http_method_override'; inputTag.type='hidden'; inputTag.name='X-HTTP-Method-Override'; inputTag.value='DELETE'; form.appendChild( inputTag );"" ><label for=""{0}_{1}_delete"">DELETE</label><br />",
controllerName, methodName);
		}
		private static void renderAntiForgeryToken(ViewContext viewContext, StringBuilder html, MethodInfo m)
		{
			if (m.GetCustomAttributes(typeof(ValidateAntiForgeryTokenAttribute), true).Length > 0)
			{
				var salt = getTokenSalt(m);
				if (!string.IsNullOrEmpty(salt))
				{
					html.Append(AntiForgery.GetHtml(viewContext.HttpContext, salt, "", "").ToHtmlString());
				}
				else
				{
					html.Append(AntiForgery.GetHtml().ToHtmlString());
				}
			}
		}

		private static string getDescription(ParameterInfo parameter)
		{
			var attributes = parameter.GetCustomAttributes(typeof(System.ComponentModel.DescriptionAttribute), false);
			return attributes.Length > 0 ? ((DescriptionAttribute)attributes[0]).Description : string.Empty;
		}
		private static string getDescription(PropertyInfo property)
		{
			var attributes = property.GetCustomAttributes(typeof(System.ComponentModel.DescriptionAttribute), false);
			return attributes.Length > 0 ? ((DescriptionAttribute)attributes[0]).Description : string.Empty;
		}
		private static string getDescription(MethodInfo method)
		{
			var attributes = method.GetCustomAttributes(typeof(System.ComponentModel.DescriptionAttribute), false);
			return attributes.Length > 0 ? ((DescriptionAttribute)attributes[0]).Description : string.Empty;
		}

		private static bool isGeneralField(Type fieldType)
		{
			return fieldType.IsPrimitive
						|| fieldType.Equals(typeof(string))
						|| fieldType.Equals(typeof(decimal))
						|| (fieldType.IsGenericType && fieldType.GetGenericTypeDefinition() == typeof(Nullable<>));//Determine whether a field type object represents a Nullable type
		}
		private static bool isFileUploadField(Type fieldType)
		{
			return fieldType.Equals(typeof(HttpPostedFileBase));
		}
		private static bool isDerivedFromGenericType(Type type, Type genericType)
		{
			if (!type.IsGenericType)
			{
				return false;
			}
			else if (type.GetGenericTypeDefinition().Equals(genericType))
			{
				return true;
			}
			else
			{
				foreach (var interfaces in type.GetInterfaces())
				{
					if (interfaces.GetGenericTypeDefinition().Equals(genericType))
					{
						return true;
					}
				}
			}
			
			if (type.BaseType == null)
			{
				return false;
			}
			
			return isDerivedFromGenericType(type.BaseType, genericType);
		}

		/// <summary>
		/// Try to get the action url with specific route name.
		/// </summary>
		private string getActionUrl(string routeName, ViewContext viewContext, string controllerName, string actionName)
		{
			var url = UrlHelper.GenerateUrl(routeName, actionName, controllerName, null, RouteTable.Routes, viewContext.RequestContext, true);
			if (url == null)
			{
				url = getActionUrl(viewContext, controllerName, actionName);
			}

			return url;
		}

		/// <summary>
		/// Try to get the first matched action url.
		/// </summary>
		private string getActionUrl(ViewContext viewContext, string controllerName, string actionName)
		{
			var url = "";
			FieldInfo field = typeof(RouteCollection).GetField("_namedMap", BindingFlags.Instance | BindingFlags.NonPublic);
			Dictionary<string, RouteBase> namedMap = field.GetValue(RouteTable.Routes) as Dictionary<string, RouteBase>;
			foreach (var route in namedMap)
			{
				url = UrlHelper.GenerateUrl(route.Key, actionName, controllerName, null, RouteTable.Routes, viewContext.RequestContext, true);
				if (url != null)
				{
					break;
				}
			}
			return url;
		}

		private static string simplifyTypeName(Type type)
		{
			var result = "";
			if (type.Equals(typeof(Byte)))
			{
				result = "byte";
			}
			else if (type.Equals(typeof(SByte)))
			{
				result = "sbyte";
			}
			else if (type.Equals(typeof(Int16)))
			{
				result = "short";
			}
			else if (type.Equals(typeof(UInt16)))
			{
				result = "ushort";
			}
			else if (type.Equals(typeof(Int32)))
			{
				result = "int";
			}
			else if (type.Equals(typeof(UInt32)))
			{
				result = "uint";
			}
			else if (type.Equals(typeof(Int64)))
			{
				result = "long";
			}
			else if (type.Equals(typeof(UInt64)))
			{
				result = "ulong";
			}
			else if (type.Equals(typeof(Single)))
			{
				result = "float";
			}
			else if (type.Equals(typeof(Double)))
			{
				result = "double";
			}
			else if (type.Equals(typeof(Decimal)))
			{
				result = "decimal";
			}
			else if (type.Equals(typeof(Char)))
			{
				result = "char";
			}
			else if (type.Equals(typeof(String)))
			{
				result = "string";
			}
			else if (type.Equals(typeof(Boolean)))
			{
				result = "bool";
			}
			else if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>))
			{
				var arg = type.GetGenericArguments()[0];
				result = simplifyTypeName(arg) + "?";
			}
			else if (type.Equals(typeof(HttpPostedFileBase)))
			{
				result = "file";
			}
			else
			{
				result = type.ToString();
			}
			return result;
		}
		
		private static string getTokenSalt(MethodInfo m)
		{
			string result = "";
			foreach (var attrData in m.GetCustomAttributesData())
			{
				foreach (var argument in attrData.NamedArguments)
				{
					if (argument.MemberInfo.Name == "Salt")
					{
						result = argument.TypedValue.Value.ToString();
						break;
					}
				}
			}
			return result;
		}

		private static string getActionName(MethodInfo m)
		{
			string result = m.Name;
			var attrs = m.GetCustomAttributes(typeof(ActionNameAttribute), true);
			if(attrs.Length > 0)
			{
				var attr = ((ActionNameAttribute)attrs[0]);
				result = attr.Name;
			}
			return result;
		}
	}
}
